/**
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */
package org.evosuite.graphs.cfg;

import java.util.*;

import org.evosuite.coverage.branch.BranchPool;
import org.evosuite.runtime.instrumentation.AnnotatedLabel;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>
 * BytecodeInstructionPool class.
 * </p>
 * 
 * @author Andre Mis
 */
public class BytecodeInstructionPool {

	private static Logger logger = LoggerFactory.getLogger(BytecodeInstructionPool.class);

	private static Map<ClassLoader, BytecodeInstructionPool> instanceMap = new LinkedHashMap<>();

	private final ClassLoader classLoader;

	private BytecodeInstructionPool(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	public static BytecodeInstructionPool getInstance(ClassLoader classLoader) {
		if (!instanceMap.containsKey(classLoader)) {
			instanceMap.put(classLoader, new BytecodeInstructionPool(classLoader));
		}

		return instanceMap.get(classLoader);
	}

	// maps className -> method inside that class -> list of
	// BytecodeInstructions
	private final Map<String, Map<String, List<BytecodeInstruction>>> instructionMap = new LinkedHashMap<>();

	private final List<MethodNode> knownMethodNodes = new ArrayList<>();

	// fill the pool

	/**
	 * Called by each CFGGenerator for it's corresponding method.
	 * 
	 * The MethodNode contains all instructions within a method. A call to
	 * registerMethodNode() fills the instructionMap of the
	 * BytecodeInstructionPool with the instructions in that method and returns
	 * a List containing the BytecodeInstructions within that method.
	 * 
	 * While registering all instructions the lineNumber of each
	 * BytecodeInstruction is set.
	 * 
	 * @param node
	 *            a {@link org.objectweb.asm.tree.MethodNode} object.
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 * @return a {@link java.util.List} object.
	 */
	public List<BytecodeInstruction> registerMethodNode(MethodNode node,
	        String className, String methodName) {
		registerMethodNode(node);

		int lastLineNumber = -1;
		int bytecodeOffset = 0;

		for (int instructionId = 0; instructionId < node.instructions.size(); instructionId++) {
			AbstractInsnNode instructionNode = node.instructions.get(instructionId);

			BytecodeInstruction instruction = BytecodeInstructionFactory.createBytecodeInstruction(classLoader,
			                                                                                       className,
			                                                                                       methodName,
			                                                                                       instructionId,
			                                                                                       bytecodeOffset,
			                                                                                       instructionNode);

			if (instruction.isLineNumber())
				lastLineNumber = instruction.getLineNumber();
			else if (lastLineNumber != -1)
				instruction.setLineNumber(lastLineNumber);

			bytecodeOffset += getBytecodeIncrement(instructionNode);

			if (!instruction.isLabel() && !instruction.isLineNumber()
			        && !instruction.isFrame()) {
				bytecodeOffset++;
			}

			registerInstruction(instruction);

		}

		List<BytecodeInstruction> r = getInstructionsIn(className, methodName);
		if (r == null || r.size() == 0)
			throw new IllegalStateException(
			        "expect instruction pool to return non-null non-empty list of instructions for a previously registered method "
			                + methodName);

		return r;
	}

	/**
	 * Determine how many bytes the current instruction occupies together with
	 * its operands
	 * 
	 * @return
	 */
	private int getBytecodeIncrement(AbstractInsnNode instructionNode) {
		int opcode = instructionNode.getOpcode();
		switch (opcode) {
		case Opcodes.ALOAD: // index
		case Opcodes.ASTORE: // index
		case Opcodes.DLOAD:
		case Opcodes.DSTORE:
		case Opcodes.FLOAD:
		case Opcodes.FSTORE:
		case Opcodes.ILOAD:
		case Opcodes.ISTORE:
		case Opcodes.LLOAD:
		case Opcodes.LSTORE:
			VarInsnNode varNode = (VarInsnNode) instructionNode;
			if (varNode.var > 3)
				return 1;
			else
				return 0;
		case Opcodes.BIPUSH: // byte
		case Opcodes.NEWARRAY:
		case Opcodes.RET:
			return 1;
		case Opcodes.LDC:
			LdcInsnNode ldcNode = (LdcInsnNode)instructionNode;
			if(ldcNode.cst instanceof Double || ldcNode.cst instanceof Long)
				return 2; // LDC2_W
			else
				return 1;
		case 19: //LDC_W
		case 20: //LDC2_W
			return 2;		
		case Opcodes.ANEWARRAY: // indexbyte1, indexbyte2
		case Opcodes.CHECKCAST: // indexbyte1, indexbyte2
		case Opcodes.GETFIELD:
		case Opcodes.GETSTATIC:
		case Opcodes.GOTO:
		case Opcodes.IF_ACMPEQ:
		case Opcodes.IF_ACMPNE:
		case Opcodes.IF_ICMPEQ:
		case Opcodes.IF_ICMPNE:
		case Opcodes.IF_ICMPGE:
		case Opcodes.IF_ICMPGT:
		case Opcodes.IF_ICMPLE:
		case Opcodes.IF_ICMPLT:
		case Opcodes.IFLE:
		case Opcodes.IFLT:
		case Opcodes.IFGE:
		case Opcodes.IFGT:
		case Opcodes.IFNE:
		case Opcodes.IFEQ:
		case Opcodes.IFNONNULL:
		case Opcodes.IFNULL:
		case Opcodes.IINC:
		case Opcodes.INSTANCEOF:
		case Opcodes.INVOKESPECIAL:
		case Opcodes.INVOKESTATIC:
		case Opcodes.INVOKEVIRTUAL:
		case Opcodes.JSR:
		case Opcodes.NEW:
		case Opcodes.PUTFIELD:
		case Opcodes.PUTSTATIC:
		case Opcodes.SIPUSH:
			// case Opcodes.LDC_W
			// case Opcodes.LDC2_W

			return 2;
		case Opcodes.MULTIANEWARRAY:
			return 3;
		case Opcodes.INVOKEDYNAMIC:
		case Opcodes.INVOKEINTERFACE:
			return 4;

		case Opcodes.LOOKUPSWITCH:
		case Opcodes.TABLESWITCH:
			// TODO: Could be more
			return 4;
			// case Opcodes.GOTO_W 
			// case Opcodes.JSR_W
		}
		return 0;
	}

	private void registerMethodNode(MethodNode node) {
		for (MethodNode mn : knownMethodNodes)
			if (mn == node)
				logger.debug("CFGGenerator.analyze() apparently got called for the same MethodNode twice");

		knownMethodNodes.add(node);
	}

	/**
	 * <p>
	 * registerInstruction
	 * </p>
	 * 
	 * @param instruction
	 *            a {@link org.evosuite.graphs.cfg.BytecodeInstruction} object.
	 */
	public void registerInstruction(BytecodeInstruction instruction) {
		String className = instruction.getClassName();
		String methodName = instruction.getMethodName();

		if (!instructionMap.containsKey(className))
			instructionMap.put(className,
			                   new LinkedHashMap<>());
		if (!instructionMap.get(className).containsKey(methodName))
			instructionMap.get(className).put(methodName,
			                                  new ArrayList<>());

		instructionMap.get(className).get(methodName).add(instruction);
		logger.debug("Registering instruction "+instruction);
		List<BytecodeInstruction> instructions = instructionMap.get(className).get(methodName);
		if(instructions.size() > 1) {
			BytecodeInstruction previous = instructions.get(instructions.size() - 2);
			if(previous.isLabel()) {
				LabelNode ln = (LabelNode)previous.asmNode;
				if (ln.getLabel() instanceof AnnotatedLabel) {
					AnnotatedLabel aLabel = (AnnotatedLabel) ln.getLabel();
					if(aLabel.isStartTag()) {
						if(aLabel.shouldIgnore()) {
							logger.debug("Ignoring artificial branch: "+instruction);
							return;
						}
					}
				}
			}
		}
		
		if (instruction.isActualBranch()) {
			BranchPool.getInstance(classLoader).registerAsBranch(instruction);
		}
	}

	// retrieve data from the pool

	/**
	 * <p>
	 * getInstruction
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 * @param instructionId
	 *            a int.
	 * @param asmNode
	 *            a {@link org.objectweb.asm.tree.AbstractInsnNode} object.
	 * @return a {@link org.evosuite.graphs.cfg.BytecodeInstruction} object.
	 */
	public BytecodeInstruction getInstruction(String className, String methodName,
	        int instructionId, AbstractInsnNode asmNode) {

		BytecodeInstruction r = getInstruction(className, methodName, instructionId);

		if (r != null)
			assert (r.sanityCheckAbstractInsnNode(asmNode));

		return r;
	}

	/**
	 * <p>
	 * getInstruction
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 * @param instructionId
	 *            a int.
	 * @return a {@link org.evosuite.graphs.cfg.BytecodeInstruction} object.
	 */
	public BytecodeInstruction getInstruction(String className, String methodName,
	        int instructionId) {

		if (instructionMap.get(className) == null) {
			logger.debug("unknown class: " + className);
			logger.debug(instructionMap.keySet().toString());
			return null;
		}
		if (instructionMap.get(className).get(methodName) == null) {
			logger.debug("unknown method: " + methodName);
			logger.debug(instructionMap.get(className).keySet().toString());
			return null;
		}
		for (BytecodeInstruction instruction : instructionMap.get(className).get(methodName)) {
			if (instruction.getInstructionId() == instructionId)
				return instruction;
		}

		logger.debug("unknown instruction " + instructionId + ", have "
		        + instructionMap.get(className).get(methodName).size());
		for (int i = 0; i < instructionMap.get(className).get(methodName).size(); i++) {
			logger.info(instructionMap.get(className).get(methodName).get(i).toString());
		}

		return null;
	}

	/**
	 * <p>
	 * getInstruction
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 * @param node
	 *            a {@link org.objectweb.asm.tree.AbstractInsnNode} object.
	 * @return a {@link org.evosuite.graphs.cfg.BytecodeInstruction} object.
	 */
	public BytecodeInstruction getInstruction(String className, String methodName,
	        AbstractInsnNode node) {

		if (instructionMap.get(className) == null) {
			logger.debug("unknown class: " + className);
			logger.debug(instructionMap.keySet().toString());
			return null;
		}
		if (instructionMap.get(className).get(methodName) == null) {
			logger.debug("unknown method: " + methodName);
			logger.debug(instructionMap.get(className).keySet().toString());
			return null;
		}
		for (BytecodeInstruction instruction : instructionMap.get(className).get(methodName)) {
			if (instruction.asmNode == node)
				return instruction;
		}

		logger.debug("unknown instruction: " + node + ", have "
		        + instructionMap.get(className).get(methodName).size()
		        + " instructions for this method");
		logger.debug(instructionMap.get(className).get(methodName).toString());

		return null;
	}

	/**
	 * <p>
	 * knownClasses
	 * </p>
	 * 
	 * @return a {@link java.util.Set} object.
	 */
	public Set<String> knownClasses() {
		return new LinkedHashSet<>(instructionMap.keySet());
	}

	/**
	 * <p>
	 * knownMethods
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @return a {@link java.util.Set} object.
	 */
	public Set<String> knownMethods(String className) {
		Set<String> r = new LinkedHashSet<>();

		if (instructionMap.get(className) != null)
			r.addAll(instructionMap.get(className).keySet());

		return r;
	}

	public boolean hasMethod(String className, String methodName) {
		if (instructionMap.get(className) != null)
			return instructionMap.get(className).containsKey(methodName);

		return false;
	}

	/**
	 * <p>
	 * getInstructionsIn
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 * @return a {@link java.util.List} object.
	 */
	public List<BytecodeInstruction> getInstructionsIn(String className, String methodName) {
		if (instructionMap.get(className) == null
		        || instructionMap.get(className).get(methodName) == null)
			return null;

		List<BytecodeInstruction> r = new ArrayList<>();
		r.addAll(instructionMap.get(className).get(methodName));

		return r;
	}
	
	public List<BytecodeInstruction> getInstructionsIn(String className) {
		if (instructionMap.get(className) == null)
			return null;

		List<BytecodeInstruction> r = new ArrayList<>();
		Map<String, List<BytecodeInstruction>> methodMap = instructionMap.get(className);
		for(List<BytecodeInstruction> methodInstructions : methodMap.values()) {
			r.addAll(methodInstructions);
		}
		
		return r;
	}
	
	public List<BytecodeInstruction> getAllInstructions() {
		List<BytecodeInstruction> r = new ArrayList<>();
		for(String className : instructionMap.keySet()) {
			Map<String, List<BytecodeInstruction>> methodMap = instructionMap.get(className);
			for(List<BytecodeInstruction> methodInstructions : methodMap.values()) {
				r.addAll(methodInstructions);
			}
		}
		
		return r;
	}

	/**
	 * <p>
	 * logInstructionsIn
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 */
	public void logInstructionsIn(String className, String methodName) {

		logger.debug("Printing instructions in " + className + "." + methodName + ":");

		List<BytecodeInstruction> instructions = getInstructionsIn(className, methodName);
		if (instructions == null) {
			logger.debug("..unknown method");
		} else {
			for (BytecodeInstruction instruction : instructions) {
				logger.debug("\t" + instruction);
			}
		}

	}

	/**
	 * <p>
	 * createFakeInstruction
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 * @return a {@link org.evosuite.graphs.cfg.BytecodeInstruction} object.
	 */
	public BytecodeInstruction createFakeInstruction(String className, String methodName) {

		AbstractInsnNode fakeNode = new InsnNode(Opcodes.NOP);

		int instructionId = getInstructionsIn(className, methodName).size();

		BytecodeInstruction instruction = new BytecodeInstruction(classLoader, className,
		        methodName, instructionId, -1, fakeNode);

		registerInstruction(instruction);

		return instruction;

	}

	/**
	 * <p>
	 * clear
	 * </p>
	 */
	public void clear() {
		instructionMap.clear();
		knownMethodNodes.clear();
	}

	public static void clearAll() {
		BytecodeInstructionPool.instanceMap.clear();
	}

	/**
	 * <p>
	 * clear
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 */
	public void clear(String className) {
		instructionMap.remove(className);
	}

	public static void clearAll(String className) {
		for (BytecodeInstructionPool pool : instanceMap.values()) {
			pool.clear(className);
		}
	}

	/**
	 * <p>
	 * clear
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param methodName
	 *            a {@link java.lang.String} object.
	 */
	public void clear(String className, String methodName) {
		if (instructionMap.containsKey(className))
			instructionMap.get(className).remove(methodName);
	}

	public static void clearAll(String className, String methodName) {
		for (BytecodeInstructionPool pool : instanceMap.values()) {
			pool.clear(className, methodName);
		}
	}

	/**
	 * <p>
	 * forgetInstruction
	 * </p>
	 * 
	 * @param ins
	 *            a {@link org.evosuite.graphs.cfg.BytecodeInstruction} object.
	 * @return a boolean.
	 */
	public boolean forgetInstruction(BytecodeInstruction ins) {
		if (!instructionMap.containsKey(ins.getClassName()))
			return false;
		if (!instructionMap.get(ins.getClassName()).containsKey(ins.getMethodName()))
			return false;

		return instructionMap.get(ins.getClassName()).get(ins.getMethodName()).remove(ins);
	}

	public int getFirstLineNumberOfMethod(String className, String methodName) {
		if (instructionMap.get(className) == null)
			throw new IllegalArgumentException("unknown class " + className);
		if (instructionMap.get(className).get(methodName) == null)
			throw new IllegalArgumentException("unknown method " + methodName
			        + " in class " + className);
		if (instructionMap.get(className).get(methodName).isEmpty())
			throw new IllegalArgumentException("no instructions in method " + methodName
			        + " in class " + className);

		int r = Integer.MAX_VALUE;
		for (BytecodeInstruction ins : instructionMap.get(className).get(methodName)) {
			if (ins.getLineNumber() < r)
				r = ins.getLineNumber();
		}
		return r;
	}
	
	public BytecodeInstruction getFirstInstructionAtLineNumber(String className, String methodName, int lineNumber) {
		// TODO
		if (instructionMap.get(className) == null)
			return null;
		if (instructionMap.get(className).get(methodName) == null)
			return null;
		if (instructionMap.get(className).get(methodName).isEmpty())
			return null;

		for (BytecodeInstruction ins : instructionMap.get(className).get(methodName)) {
			if (ins.getLineNumber() == lineNumber)
				return ins;
		}
		return null;
	}
}
